using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace ProtoScratch.Common
{
	abstract class ScratchProtocol
	{
		protected readonly ScratchSession session;
		protected readonly NetworkStream netStream;
		protected readonly ScratchEvents handler;
		protected readonly ScratchEncryption encryptor = new ScratchEncryption();
		protected bool aborted;
		protected int sessionId;
		private Thread sessionThread;

		protected ScratchProtocol(ScratchSession session, NetworkStream netStream)
		{
			this.session = session;
			this.netStream = netStream;
			handler = session.create();
			encryptor.rsaKey = session.rsaKey;
		}
		
		protected static class Internal
		{
			//Primary Commands...
			public const int Config = 1;
			public const int PingPong = 10;
			
			//Config Secondaries...
			public const int Confirm = 1;
			
			//PingPong Secondaries...
			public const int Ping = 1;
			public const int Pong = 2;
		}
		
		protected void Run(string threadName)
		{
			sessionThread = new Thread(HandleSession);
			sessionThread.Name = threadName;
			sessionThread.IsBackground = true;
			sessionThread.Start();
		}
		
		protected abstract void HandleSession();
		
		 protected ScratchMessage Receive()
        {
            List<ScratchMessage> Messages = new List<ScratchMessage>();
            do Messages.Add(ReceiveFrame());
            while (Messages[Messages.Count - 1].getFlag(ScratchFlags.Partial));

            if (Messages.Count > 1)
            {
                List<byte> Data = new List<byte>();
                for (int i = 0; i < Messages.Count; i++)
                    Data.AddRange(Messages[i].getData());

                return new ScratchMessage(Messages[0].Channel, Messages[0].PrimaryCommand, Messages[0].SecondaryCommand,
                    Data.ToArray(), Messages[Messages.Count - 1].Flags, Messages[0].SessionID);
            }
            else
                return Messages[0];
        }
		
		protected ScratchMessage ReceiveFrame()
		{
			byte[] Header = new byte[1];
			//keep reading until we get a key match. we read 1 byte at a time as to ensure we don't "waste" data
			for (int i = 0; i < 4; i++)
			{
				if (netStream.Read(Header, 0, 1) == 0)
					throw new ScratchException(ScratchException.eType.Timeout, "Timeout Occurred");
				if (Header[0] != session.key[i])
					i = -1; //key does not match, restart loop
			}
			
			Header = ReadUntil(15); //This is the rest of the header after "Key"
			int sID = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(Header, 2));
			ushort Flags = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(Header, 6));
			if (((Flags & Internal.Config) != Internal.Config && sID != sessionId) ||
			    Header[0] != session.vMajor || Header[1] != session.vMinor)
			{
				throw new ScratchException(ProtoScratch.Common.ScratchException.eType.MalformedFrame, "Invalid Protocol Frame");
			}
			byte Channel = Header[8];
			short PrimaryCommand = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(Header, 9));
			short SecondaryCommand = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(Header, 11));
			ushort Length = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(Header, 13));
			byte[] Data = ReadUntil(Length);
			
			if ((Flags & ScratchFlags.Encrypted) == ScratchFlags.Encrypted)
				Data = encryptor.Decrypt(Data);
			if ((Flags & ScratchFlags.Compressed) == ScratchFlags.Compressed)
				Data = ScratchCompression.Decompress(Data);
			
			return new ScratchMessage(Channel, PrimaryCommand, SecondaryCommand, Data, Flags, sID);
		}
		
		protected void Send(ScratchMessage rawMessage)
		{
			lock (this)
            {
                foreach (var Message in rawMessage.split())
                {
                    var ContentData = Message.getData();
                    if (Message.getFlag(ScratchFlags.Compressed))
                        ContentData = ScratchCompression.Compress(ContentData);
                    if (Message.getFlag(ScratchFlags.Encrypted))
                        ContentData = encryptor.Encrypt(ContentData);

                    var Data = new byte[19 + ContentData.Length];
                    Array.Copy(session.key, Data, 4);
                    Data[4] = session.vMajor;
                    Data[5] = session.vMinor;
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(sessionId)), 0, Data, 6, 4);
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)Message.Flags)), 0, Data, 10, 2);
                    Data[12] = Message.Channel;
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Message.PrimaryCommand)), 0, Data, 13, 2);
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Message.SecondaryCommand)), 0, Data, 15, 2);
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)ContentData.Length)), 0, Data, 17, 2);
                    Array.Copy(ContentData, 0, Data, 19, ContentData.Length);
                    netStream.Write(Data, 0, Data.Length);
                }
            }
		}
		
		private byte[] ReadUntil(int Size)
		{
			var Result = new byte[Size];
			int Offset = 0;
			while (Size > 0)
			{
				int ThisRead = netStream.Read(Result, Offset, Size);
				if (ThisRead == 0)
					throw new ScratchException(ScratchException.eType.Timeout, "Timeout Occurred");
				Offset += ThisRead;
				Size -= ThisRead;
			}
			return Result;
		}
		
		/// <summary>
		/// Close the connection and end the thread.
		/// </summary>
		protected void Abort()
		{
			aborted = true;
			netStream.Close();
		}
		
		protected void Ping()
		{
			Send(new ScratchMessage(0,Internal.PingPong, Internal.Ping));
		}
		
		protected void Pong()
		{
			Send(new ScratchMessage(0,Internal.PingPong, Internal.Pong));
		}
	}

	/// <summary>
	/// ScratchFlags contains the bit definitions for each individual flag possibility in the header.
	/// </summary>
	static class ScratchFlags
	{
		public const ushort Internal = 0x0001;
		public const ushort Encrypted = 0x0002;
		public const ushort Compressed = 0x0004;
        public const ushort Partial = 0x0008;
	}
}
